Customer Churn (ou Rotatividade de Clientes, em uma tradução livre) refere-se a uma decisão tomada pelo cliente sobre o término do relacionamento comercial. Refere-se também à perda de clientes. A fidelidade do cliente e a rotatividade de clientes sempre somam 100%.
Nesse projeto utilizaremos ferramentas da estatísticas para nos ajudar a retirar informações dos dados que auxiliem numa melhor tomada de decisão. Além disso, através de gráficos conseguiremos identificar facilmente onde é necessário maiores esforços para retenção dos clintes, ou seja, onde existe maior percentual de churn.
Através do algoritmo kmeans realizaremos segmentação dos clientes para identificarmos quais os clientes que mais fazem ligações e quais os clientes que mais gastam minutos por ligação.
Por fim, criaremos um algoritmo para prever se um cliente pode ou não cancelar seu plano e qual a probabilidade de isso ocorrer.
Será considerado um algoritmo válido caso possua uma acurácia de ao menos 80%.
# Importanto as bibliotecas básicas necessárias para o processo de Data Munging.
# Ignorando mensagens de avisos.
import warnings
warnings.filterwarnings("ignore")
# Importando o Numpy e Pandas
import numpy as np
import pandas as pd
# Importando pacotes para visualização.
import matplotlib.pyplot as plt
%matplotlib inline
import seaborn as sns
import joypy as joy
import plotly.graph_objects as go
import plotly.offline as pyo
from plotly.subplots import make_subplots
pyo.init_notebook_mode()
# pacote de statística
from scipy.stats import binom
from scipy.stats import probplot
# Lendo os dados de treino e de teste.
treino = 'projeto4_telecom_treino.csv'
teste = 'projeto4_telecom_teste.csv'
dados_treino_bruto = pd.read_csv(treino)
dados_teste_bruto = pd.read_csv(teste)
# Verificando o shape dos dados.
print('Dados de treino: ', dados_treino_bruto.shape)
print('Dados de teste: ', dados_teste_bruto.shape)
Vejamos algumas informações sobre como as variáveis foram carregadas nos dados de treino.
# Informações sobre os dados de treino.
dados_treino_bruto.info()
Aparentemente não temos nenhuma variável com valor nulo. Contudo, vamos garantir que realmente não temos valores nulos e nem NA.
# Verificando se há valores nulos ou NA's.
print('Valores NAs nos dados de treino: ', dados_treino_bruto.isna().sum().sum())
print('Valores nulos nos dados de treino: ', dados_treino_bruto.isnull().sum().sum())
print('Valores duplicados: ', dados_treino_bruto.duplicated().sum())
# 5 primeiras linhas.
dados_treino_bruto.head()
Podemos ver que temos uma coluna chamada Unnamed que aparentemente se trata do indice dos dados. Faremos sua remoção e em seguida passaremos à algumas análises exploratória nos dados.
# Removendo a coluna Unnamed
dados_treino_bruto = dados_treino_bruto.drop('Unnamed: 0', axis=1)
# Verificando quantidade de estados.
print('Quantidade total de estados: ', len(dados_treino_bruto['state'].unique()))
Para facilitar a leitura dos estados faremos a conversão da sigla ao nome do estado utilizando o dataset disponibilizado pelo site: github - cphalpert.
# realizando a leitura do dataset.
states_name = pd.read_csv('states_name.csv')
# Imprimindo as 5 primeiras linhas dos dados.
states_name.head()
# Removendo a coluna Division
states_name = states_name.drop('Division', axis=1)
# Alterando o nome da coluna State code para state
states_name = states_name.rename(columns={'State Code': 'state'})
# Validando a transformação
states_name.head()
# Agregando o nome e região dos estados aos nossos dados.
dados_treino_bruto = dados_treino_bruto.merge(states_name, sort=True)
# removendo a coluna state antiga
dados_treino_bruto = dados_treino_bruto.drop('state', axis=1)
# Verificando as 5 primeiras linhas
dados_treino_bruto.head()
# Verificando se foram gerados valores nulos em nossa junção.
print('valores NA nas colunas:\n',dados_treino_bruto[['State', 'Region']].isna().sum())
print('\nvalores Nulos nas colunas:\n',dados_treino_bruto[['State', 'Region']].isnull().sum())
# Salvando os dados num arquivo csv.
dados_treino_bruto.to_csv('treino_state_region.csv')
dados_treino_bruto.columns = ['tempo_conta', 'codigo_area', 'srv_internacional', 'srv_caixa_postal', 'mensagens_voz',
'minutos_lig_manha', 'ligacoes_manha', 'taxa_lig_manha', 'minutos_lig_tarde', 'ligacoes_tarde',
'taxa_lig_tarde', 'minutos_lig_noite', 'ligacoes_noite', 'taxa_lig_noite', 'minutos_lig_internacional',
'ligacoes_internacional', 'taxa_lig_internacional', 'ligacoes_SAC', 'churn', 'estado', 'regiao']
dados_treino_bruto['minutos_total'] = dados_treino_bruto[[col for col in dados_treino_bruto.columns \
if 'minutos' in col]].sum(axis=1)
dados_treino_bruto['ligacoes_total'] = dados_treino_bruto[[col for col in dados_treino_bruto.columns \
if 'ligacoes' in col]].sum(axis=1)
dados_treino_bruto['taxa_total'] = dados_treino_bruto[[col for col in dados_treino_bruto.columns \
if 'taxa' in col]].sum(axis=1)
conditions = [
(dados_treino_bruto['srv_internacional'] == 'yes') & (dados_treino_bruto['srv_caixa_postal'] == 'yes'),
(dados_treino_bruto['srv_internacional'] == 'yes') | (dados_treino_bruto['srv_caixa_postal'] == 'yes'),
(dados_treino_bruto['srv_internacional'] == 'no') & (dados_treino_bruto['srv_caixa_postal'] == 'no')]
choices = [int(2), int(1), int(0)]
dados_treino_bruto['Qntd_servicos'] = np.select(conditions, choices, default=np.nan)
dados_treino_bruto['churn'] = [0 if x=='no' else 1 for x in dados_treino_bruto['churn']]
media_churn = dados_treino_bruto['churn'].mean()
dados_treino_bruto.groupby('churn').size() / len(dados_treino_bruto)
O percentual de churn é de ~15%. Utilizaremos a função massa de probabilidade binominal para nos ajudar a identificar qual a probabilidade de um número determinado de clientes dar churn.
# criando um range de clientes para verificar probabilidade de churn.
tamanho = range(10, 1000, 10)
# Aplicando a função massa de probabilidade binominal.
probs = binom.pmf(tamanho, len(dados_treino_bruto), media_churn)
fig, ax = plt.subplots(figsize=(15,10))
plt.stem(tamanho, probs)
plt.ylabel('Probabilidade', size=15)
plt.xlabel('Número de churns (clientes)', size=15)
É possível notar que a maior probabilidade de churn está em torno de 500 clientes. Vamos utilizar a função distribuição acumulada para verificarmos a probabilidade de que até 500 clientes realizem churn com uma média de 14,4914% encontrada anteriormente.
# Aplicando a função distribuição acumulada.
binom.cdf(500, 3333, media_churn)
Vemos que a probabilidade de que não mais 500 clientes deêm churn com uma média de 14,4914% é de 80%. Disso podemos dizer que as chances de que mais de 500 dêem churn é de 20%.
Criaremos uma classe para nos ajudar com as análises.
# classe para análises.
class DatAnalysis:
def __init__(self):
pass
# Gráfico de hisotrama e QQPlot
def probahist(self, dados, coluna):
fig, ax = plt.subplots(figsize=(8,6))
probplot(dados[coluna], plot=plt)
plt.title('QQPlot - {}'.format(coluna), size=20)
# Histograma
fig = go.Figure()
fig.add_trace(
go.Histogram(x=dados[coluna], name='', marker_color='#342870', histnorm='probability'))
fig.update_layout(title_text='Histograma <b>{}</b>'.format(coluna),
plot_bgcolor='white',
xaxis=dict(
title=coluna), height=500, width=800)
fig.show()
# Percentual de clientes por variável.
def Scatter_churn(self, data, coluna):
if (coluna=='churn'):
return print('Informar coluna numérica diferente de churn')
if (data[coluna].dtypes=='int64' or data[coluna].dtypes=='float64'):
grouped = data.groupby(coluna)['churn'].mean()
fig = go.Figure()
fig.add_trace(go.Scatter(
name='',
mode='markers',
y=grouped.values,
x=grouped.index,
marker=dict(color='#45d96a',
size=7,
line= dict(width=1))))
fig.update_xaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0, 0, 0, 0.12)')
fig.update_yaxes(showgrid=True, gridwidth=1, gridcolor='rgba(0, 0, 0, 0.12)')
fig.update_layout(title_text='Correlação de <b>{}</b> com a taxa de <b>churn</b>'.format(coluna),
plot_bgcolor='white',
xaxis=dict(
title=coluna),
yaxis=dict(
title='Taxa de Churn'))
# Criando uma variável para plotagem da correlação entre as variáveis.
correlacao = data.groupby(coluna)[['churn']].mean().reset_index().corr()['churn'][0]
fig.add_annotation(text=f"<b>Correlação: {round(correlacao, 2)}</b>",
xref="paper", yref="paper", font=dict(color="#008a0e"),
x=0, y=1.1, showarrow=False)
fig.show()
else:
print(f'{coluna} Não é uma variável numérica')
# Função para comparar percentual de churn por agrupamento (percentual de todos os valores).
def PercentChurn(self, data, coluna):
dados = data.copy()
dados[coluna] = dados[coluna].astype('category')
grouped = dados.groupby(coluna)['churn'].mean().sort_values(ascending=False)
fig = go.Figure()
fig.add_trace(go.Bar(
name='',
y=grouped.index,
x=grouped.values*100,
orientation='h',
marker_color='#9fa2c7', hovertemplate='%{x:.2f}%'))
if len(grouped.index) > 10:
ht=1000
else:
ht=400
fig.update_layout(title_text='Percentual de Churn - <b>{}</b>'.format(coluna),
height=ht, plot_bgcolor='white',
xaxis=dict(
title='Taxa de churn',
titlefont_size=16),
yaxis=dict(
tickmode = 'linear'))
fig.show()
# Função para avaliar a média de variáveis por agrupamento.
def clientes_box(self, data, grupo, coluna, lines=10):
fig = make_subplots(rows=2, cols=2, row_heights=[0.33, 0.33],
specs=[[{"type": "bar"}, {"type": "bar"}],
[{"type": "box", "colspan": 2}, None]],
subplot_titles=("Percentual de clientes - <b>{}</b>".format(grupo),
"Quantidade de clientes - <b>{}</b>".format(grupo),
"Boxplot - {} X {}".format(grupo, coluna)),
vertical_spacing = 0.10)
# Gráfico de percentual de clientes (1x1)
dados = data.copy()
dados[grupo] = dados[grupo].astype('category')
grouped = pd.DataFrame()
grouped['percent'] = dados[grupo].value_counts() / dados[grupo].value_counts().sum() * 100
grouped['clientes'] = dados[grupo].value_counts()
grouped.sort_values(by='percent', ascending=False, inplace=True)
grouped = grouped.head(lines)
fig.add_trace(go.Bar(
name='',
y=grouped.index,
x=grouped['percent'],
orientation='h',
marker_color='#7b8c26', hovertemplate='%{x:.2f}%'), row=1, col=1)
# Gráfico de Quantidade de clientes (1x2)
fig.add_trace(go.Bar(
name='',
y=grouped.index,
x=grouped['clientes'],
orientation='h',
marker_color='#ebc334', hovertemplate='%{x} Clientes'), row=1, col=2)
# Box plot (2x2)
grouped3 = data[data[grupo].isin(grouped.index)]
fig.add_trace(
go.Box(y=grouped3[coluna], x=grouped3[grupo], name='', boxpoints='outliers', boxmean='sd',
marker_color='#342870'),row=2, col=1)
fig.update_xaxes(title_text=grupo, row=2, col=1)
fig.update_yaxes(title_text=coluna, showgrid=True, gridcolor='#b5b5b5', gridwidth=0, row=2, col=1)
if (lines>29):
ht = 1800
wd = 1400
else:
ht = 800
wd =1000
fig.update_layout(title=dict(text='<b>Dashboard Analítico - {} x {}</b>'.format(grupo, coluna),
font_size=20),
height=ht, width=wd, plot_bgcolor='white',
xaxis=dict(
titlefont_size=16),
yaxis=dict(
tickmode = 'linear'), showlegend=False)
fig.show()
# Criando uma instância da classe
analisar = DatAnalysis()
Vamos iniciar nossas análises verificando o percentual de churn para cada variável categórica de nossos dados.
# Criando uma lista apenas com as variáveis categóricas e numéricas
categoricas = [x for x in dados_treino_bruto.columns if dados_treino_bruto[x].dtypes=='object']
numericas = [x for x in dados_treino_bruto.columns if (dados_treino_bruto[x].dtypes=='int64' or \
dados_treino_bruto[x].dtypes=='float64')]
# Removendo a variável churn.
numericas.remove('churn')
# Verificando o percentual de churn por código de área.
analisar.PercentChurn(dados_treino_bruto, categoricas[0])
Não vemos diferença significativa na taxa de churn entre os códigos de área.
# Verificando o percentual de churn por plano international.
analisar.PercentChurn(dados_treino_bruto, categoricas[1])
Aqui é possível notar uma diferença bastante significativa. Clientes que possuem plano international apresentam maior tendência à churn. A diferença é de ~269%.
# Verificando o percentual de churn por serviço de caixa postal.
analisar.PercentChurn(dados_treino_bruto, categoricas[2])
Clientes que não possuem o serviço de caixa postal apresentam uma tendência à churn ~93% maior do que os clientes que possuem o plano.
# Verificando o percentual de churn por estado.
analisar.PercentChurn(dados_treino_bruto, categoricas[3])
Embora os estados de Califórnia e New Jersey apresentem as maiores taxas de churn, a partir de Arkansas todos os estados estão acima de 20% de churn.
Hawaii, Alaska, Arizona e Virgina são os estados que apresentam menor taxa de churn.
# Verificando o percentual de churn por região.
analisar.PercentChurn(dados_treino_bruto, categoricas[4])
Não é possível notar uma diferença significativa entre as regiões, entretanto, a região Northeast é a que apresenta maior taxa de churn.
# Verificando o percentual de churn por quantidade de ligaçoes ao SAC.
analisar.PercentChurn(dados_treino_bruto, numericas[14])
Podemos perceber que a partir de de 4 ligações ao SAC a chances de churn aumentam 346% em relação à 3 ligações. Necessário atenção às ligações ao SAC e tentar garantir que os clientes liguem até 3 vezes, no máximo, ao SAC. Também é interessante averiguar aos motivos dos clientes entrarem em contato com o SAC.
# Verificando o percentual de churn por quantidade de serviços.
analisar.PercentChurn(dados_treino_bruto, numericas[18])
Aqui fica fácil notar que clientes com 2 serviços (correio de voz e e plano internacional) apresentam uma taxa de churn ~186% maior que clientes com 1 ou nenhum serviço.
Não vemos diferença significativa entre clientes com 1 ou nenhum serviço.
Vejamos agora a correlação entre as variáveis numéricas e a taxa de churn.
# Correlação de churn com tempo de conta.
analisar.Scatter_churn(dados_treino_bruto, numericas[0])
Não vemos uma correlação entre o tempo da conta com a taxa de churn.
# Correlação de churn com número de mensagens.
analisar.Scatter_churn(dados_treino_bruto, numericas[1])
Aqui temos uma fraca correlação positiva, quando o número de mensagens de voz aumenta percebemos um leve aumento no percentual de churn.
# Verificando o percentual de churn por minutos totais na manhã.
analisar.Scatter_churn(dados_treino_bruto, numericas[2])
Aqui vemos uma fraca correlação positiva, onde quanto mais minutos em ligações pela manhã mais vemos um leve aumento no percentual de churn.
# Verificando o percentual de churn por total de ligaçoes na manhã.
analisar.Scatter_churn(dados_treino_bruto, numericas[3])
Não temos uma correlação entre o número de ligações com o churn.
# Verificando o percentual de churn por total de taxa na manhã.
analisar.Scatter_churn(dados_treino_bruto, numericas[4])
Aqui percebemos que temos a mesma correlação entre os minutos pela manhã e a taxa das ligações na manhã. Vejamos qual a correlação entre os minutos e as taxas em todos os períodos.
dados_treino_bruto[['minutos_lig_manha','taxa_lig_manha', 'minutos_lig_tarde', 'taxa_lig_tarde',
'minutos_lig_noite', 'taxa_lig_noite', 'minutos_lig_internacional', 'taxa_lig_internacional']].corr()
Conforme podemos ver a correlação entre os minutos e a taxa é de 100%. Ou seja, certamente quanto mais minutos são utilizamos maior será a taxa paga.
Dessa forma verificaremos a correlação apenas entre as variáveis com minutos.
# Verificando o percentual de churn por total de minutos à tarde.
analisar.Scatter_churn(dados_treino_bruto, numericas[5])
As ligações à tarde apresentam uma correlação mais fraca com o churn do que as ligações pela manhã.
# Verificando o percentual de churn por total de ligaçoes à tarde.
analisar.Scatter_churn(dados_treino_bruto, numericas[6])
As ligaçoes à tarde apresentam uma fraca correlação positiva com o percentual de churn, diferente das ligações realizadas pela manhã.
Pularemos a correlação com a taxa total à tarde pois sabemos que apresenta a mesma correlação que os minutos.
# Verificando o percentual de churn por total de minutos à noite.
analisar.Scatter_churn(dados_treino_bruto, numericas[8])
Não vemos uma correlação entre o total de minutos à noite com o churn.
# Verificando o percentual de churn por total de ligações à noite.
analisar.Scatter_churn(dados_treino_bruto, numericas[9])
Temos uma correlação positiva, entretanto, muito fraca.
# Verificando o percentual de churn por total de minutos em ligaçoes internacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[11])
Podemos ver que temos uma fraca correlação positiva, onde o aumento de minutos internacionais aumento a chances de chun.
# Verificando o percentual de churn por total de ligaçõesinternacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[12])
Aqui também temos uma fraca correlação positiva entre as o total de ligaçoes internacionais com o churn. Mas apararemente se deve aos outliers.
# Verificando o percentual de churn por total de minutos em ligaçoes internacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[14])
Uau! Aqui vemos uma forte correlação positiva, o que significa que quanto mais ligaçoes o cliente tiver ao SAC maior será suas chances de churn.
# Verificando o percentual de churn por total de minutos em ligaçoes internacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[15])
Fraca correlação positiva entre os minutos totais em ligações e a taxa de churn.
# Verificando o percentual de churn por total de minutos em ligaçoes internacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[16])
Aqui percebemos uma correlação positiva muito leve entre o total de ligações e o churn.
# Verificando o percentual de churn por total de minutos em ligaçoes internacionais.
analisar.Scatter_churn(dados_treino_bruto, numericas[17])
Aqui percebemos que a taxa total é possui uma correlação um pouco superior ao total de minutos.
Vejamos mais algumas informaçoes de nosos dados.
# Análise de serviço internacional X tempo de conta.
analisar.clientes_box(dados_treino_bruto, categoricas[0], numericas[0])
# Histograma e QQPlot - Tempo de conta.
analisar.probahist(dados_treino_bruto, numericas[0])
Podemos enxergar alguns outliers em todos os códigos de áreas, ou seja, clientes que possuem um tempo de conta consideravelmente maior que os demais.
Aqui também podemos notar que o código de área 415 é o que possui a maior concentração de clientes 49,65% ou 1655 cilentes, embora todos os códigos de áreas apresentem a mesma taxa de churn. O código de área 510 possui o cliente com maior tempo de conta 243 meses.
Também podemos ver que em ambos os códigos de áreas temos praticamente uma mesma distribuição distribuição dos dados, com uma média de conta de ~101 meses e com desvio padrão de ~40 meses.
Através do histograma conseguimos ver quea a maioria dos clientes possuem o serviço em torno de 85 à 120 meses e que os dados estão muito próximos de uma distribuição normal, portanto, podemos utilizar o teorema do limite central. Através do QQPlot confirmamos que os dados estão quase numa normal - quanto mais em cima da reta os pontos estiverem mais os dados se aproximam de uma distribuição normal.
Vejamos as demais variáveis categóricas com o tempo de conta.
# Análise de serviço internacional com tempo de conta.
analisar.clientes_box(dados_treino_bruto, categoricas[1], numericas[0])
Podemos ver que 90,31% dos clientes não possuem o serviço internacional, isso é muito bom, pois vimos anteriormente que a taxa de churn de clientes com plano internacional é ~269% superior aos que não possuem.
Aqui vemos que os clientes que deram churn possuem a conta em média 4 meses antes do que os que não deram churn.
# Análise de serviço de caixa postal com tempo de conta.
analisar.clientes_box(dados_treino_bruto, categoricas[2], numericas[0])
Aqui não temos um cenário muito bom. Vimos anteriormente que clientes que não possuem um serviço de caixa postal tendem a dar ~93% mais churn do que os clientes que possuem. Seria interassante oferecer aos clientes o serviço de caixa postal e averiguar o motivo de ainda não o terem.
Os tempos de conta dos clientes estão bem distribuidos entre os que possuem caixa postal e não.
# Análise de estados com tempo de conta.
analisar.clientes_box(dados_treino_bruto, categoricas[3], numericas[0], 51)
Aqui podemos ver que a Califórnia é um estado onde a empresa possui poucos clientes e os clientes que possuem apresentam um alto percentual de churn comparado aos demais estados. Averiguar o motivo desses clientes não gostarem dos serviços.
Em todos os estados as distribuições de tempo de conta estão muito próximas de uma normal, entretanto, é possível enxergar que no estado de Illinois existem clientes com altos tempos de conta, visto que a média é puxada para cima.
# Análise de região X tempo de conta.
analisar.clientes_box(dados_treino_bruto, categoricas[4], numericas[0])
Podemos constatar que a região Nordeste além de ser a região com menor número de clientes é a região que maior apresenta churn.
Não faremos essa análise para todas as variáveis numéricas, apenas para as: minutos_total, ligacoes_total e taxa_total.
# Análise de código de área X minutos total.
analisar.clientes_box(dados_treino_bruto, categoricas[0], numericas[-4])
# Histograma e QQPlot - Minutos Total
analisar.probahist(dados_treino_bruto, 'minutos_total')
Aqui conseguimos notar que a média de minutos em todas as três áreas é muito similar ~590 minutos. Além disso, podemos notar outliers em todas as áreas, entretanto a área 415 possui uma quantidade maior de clientes com um total de minutos inferior aos demais.
Através do histograma e QQPlot, podemos ver que os minutos totais estão muito muito próximos de uma distribuição normal e que a maior concentração de minutos está entre 560 e 619 minutos.
# Análise de serviço internacional X minutos total.
analisar.clientes_box(dados_treino_bruto, categoricas[1], numericas[-4])
Aqui vemos que entre os clientes que possuem e não o serviço internacional não vemos uma diferença na quantidade de minutos utilizados.
# Análise de serviço de caixa postal X minutos total.
analisar.clientes_box(dados_treino_bruto, categoricas[2], numericas[-4])
Mesma interpreção que ao aos clientes de serviço internacional, sem variação consirável.
# Análise de estados X minutos total.
analisar.clientes_box(dados_treino_bruto, categoricas[3], numericas[-4], 51)
Aqui conseguimos notar que o estado Connecticut apresenta a maior dispersão nos valores, um desvio padrão de 111 minutos.
E os estados Arizona e Dakota do Norte apresentam a menor dispersão de minutos - ~77 minutos.
O estado de Indiana possui a maior média de minutos e apresenta uma taxa de ~14,5% inferior à média de churn dos estados , talvez valha a pena investir para aumentar os clientes no estado.
# Análise de região X minutos total.
analisar.clientes_box(dados_treino_bruto, categoricas[4], numericas[-4])
Já em relação às regiões não vemos uma diferença significativa a não ser pelo fato de que a região Sul apresenta a maior concentração de outliers e a região Centro Oeste apresenta a maior concentração de clientes com uma quantidade de outliers abaixo do limite inferior, ou seja, é a região onde os clientes possuem a menor minutagem.
# Análise de código de área X Ligações totais.
analisar.clientes_box(dados_treino_bruto, categoricas[0], numericas[-3])
# Histograma e QQPlot - Ligações total
analisar.probahist(dados_treino_bruto, 'ligacoes_total')
Não há diferença significativa entre a quantidade de ligações, porém percebemos alguns outliers.
Através do histograma podemos notar que a maior centração de valores para a quantidade de ligações está entre 300 e 329 ligações. Também vemos que apresenta uma forma muito similar de uma distribuição normal.
# Análise de serviço internacional X Ligações totais.
analisar.clientes_box(dados_treino_bruto, categoricas[1], numericas[-3])
Não vemos diferença significação entre a quantidade de ligações e os clientes que possuem ou não serviço internacional. Embora conseguimos ver que os outliers se concentram nos clientes que não possuem serviço internacional.
# Análise de serviço de caixa postal X Ligações totais.
analisar.clientes_box(dados_treino_bruto, categoricas[2], numericas[-3])
Não vemos diferença significativa entre ligações total e serivço de caixa postal.
# Análise de estados X Ligações totais.
analisar.clientes_box(dados_treino_bruto, categoricas[3], numericas[-3], 51)
Os estados Flórida e Georgia são os estados com a maior média de ligações, entretanto, a Flórida apresenta um desvião padrão ~18% maior.
# Análise de estados X Ligações totais.
analisar.clientes_box(dados_treino_bruto, categoricas[4], numericas[-3])
Não vemos uma diferença significativa no número de ligações entre as regiões.
# Análise de código de área X Taxa total.
analisar.clientes_box(dados_treino_bruto, categoricas[0], numericas[-2])
# Histograma e QQPlot - Taxa total
analisar.probahist(dados_treino_bruto, 'taxa_total')
Sem diferença significativa na quantidade total de taxas pagas entre os códigos de áreas.
No histograma conseguimos notar que os valores de taxa pagas estão concentrados entre 56 e 60 unidades monetárias e com o QQplot constatamos que se comportam muito próximo de uma distribuição normal.
# Análise de serviço internacional X Taxa total.
analisar.clientes_box(dados_treino_bruto, categoricas[1], numericas[-2])
Sem diferença significativa.
# Análise de serviço de caixa postal X Taxa total.
analisar.clientes_box(dados_treino_bruto, categoricas[2], numericas[-2])
Sem diferença significativa.
# Análise de código de área X Taxa total.
analisar.clientes_box(dados_treino_bruto, categoricas[3], numericas[-2], 51)
Sem diferença significativa.
# Análise de código de área X Taxa total.
analisar.clientes_box(dados_treino_bruto, categoricas[4], numericas[-2])
Sem diferença significativa.
Como vimos nos boxplots temos diversos outliers em nossos dados em todas as variáveis. Vamos criar uma variável com os outliers e passar à seguimentação de clientes.
# Criando variáveis com os valores do primeiro e terceiro quartil e calculando a amplitude.
Q1 = dados_treino_bruto.quantile(0.25)
Q3 = dados_treino_bruto.quantile(0.75)
interquantile = Q3-Q1
# Criando um dataset separado para remoção dos outliers.
outliers = dados_treino_bruto[interquantile.index]
outliers = outliers[(outliers < (Q1 - 1.5* interquantile)) | (outliers > (Q3 + 1.5 * interquantile))].fillna(0)
outliers_index = []
outliers_values = {}
for coluna in outliers.columns:
outliers_values[coluna] = [valor for valor in outliers[coluna] if valor!=0 ]
outliers_index.extend([index for index, valor in enumerate(outliers[coluna]) if valor!=0 ])
outliers_index = set(outliers_index)
treino_no_outliers = dados_treino_bruto.drop(outliers_index, axis=0)
print('Total de linha nos dados: ', len(treino_no_outliers))
# Salvando os dados num arquivo csv.
treino_no_outliers.to_csv('treino_no_outliers.csv')
Para sabermos se realmente poderemos prosseguir sem os outliers, vejamos como os valores de nossa variável independente ficaram após a remoção dos outliers.
# Verificando valor COM os outliers
print('Valores da variável "CHURN" antes da remoção dos Outliers:\n\n',
dados_treino_bruto.churn.value_counts())
# Verificando valor SEM os outliers
print('\n\nValores da variável "CHURN" após remoção dos Outliers:\n\n',
treino_no_outliers['churn'].value_counts())
Conforme podemos ver acima todos os clientes com churn estão associados à algum Outlier. Sendo assim, não poderemos removê-los de nossos dados.
Passaremos à segmentação de clientes, para tanto, utilizaremos o algoritmo K-means para clusterização ou agrupamento dos clientes. Criaremos tês tipos de agrupamento: por total de minutos e ligações. Para definição da melhor quantidade de grupos utilizaremos o Elbow Method#:~:text=In%20cluster%20analysis%2C%20the%20elbow,number%20of%20clusters%20to%20use.)
# carregando o pacote Kmeans
from sklearn.cluster import KMeans
sse={}
for k in range(1, 10):
kmeans = KMeans(n_clusters=k, max_iter=1000).fit(dados_treino_bruto[['minutos_total']])
sse[k] = kmeans.inertia_
plt.figure(figsize=(8,6))
plt.plot(list(sse.keys()), list(sse.values()))
plt.xlabel("Number of cluster")
plt.show()
Ao propósito desse projeto seguiremos com um total de 3 clusters.
Bora por a mão na massa.
# Criando um loop para criação dos clusters.
for coluna in numericas[15:17]:
kmeans = KMeans(n_clusters=3).fit(dados_treino_bruto[[coluna]])
dados_treino_bruto[str('cluster_'+coluna)] = kmeans.predict(dados_treino_bruto[[coluna]])
# Verificandoo cluster de minutos
dados_treino_bruto.groupby('cluster_minutos_total')['minutos_total'].describe()
# Verificandoo cluster de ligações
dados_treino_bruto.groupby('cluster_ligacoes_total')['ligacoes_total'].describe()
# Reordenando o cluster.
dados_treino_bruto['cluster_minutos_total'] = dados_treino_bruto['cluster_minutos_total'].map({0:1, 1:2, 2:0})
dados_treino_bruto['cluster_ligacoes_total'] = dados_treino_bruto['cluster_ligacoes_total'].map({0:2, 1:0, 2:1})
# Salvando num arquivo csv.
dados_treino_bruto.to_csv('treino_clusterizado.csv')
Agora criaremos um score geral para cada cliente conforme sua classificação em cada cluster.
# Criando uma nova coluna com o score geral.
dados_treino_bruto['score_geral'] = dados_treino_bruto['cluster_minutos_total'] + dados_treino_bruto['cluster_ligacoes_total']
dados_treino_bruto.groupby('score_geral')[['minutos_total', 'ligacoes_total']] \
.mean().sort_values(['ligacoes_total', 'minutos_total'])
# Criando um subset para plotagem da segmentação dos clientes.
clusters = dados_treino_bruto[['minutos_total', 'ligacoes_total', 'score_geral']]
clusters['score_geral'] = clusters['score_geral'].map({0:'E', 1:'D', 2:'C', 3:'B', 4:'A'})
clusters['score_geral'] = clusters['score_geral'].astype('category')
fig, ax = plt.subplots(figsize=(10,8))
sns.scatterplot(data=clusters, x='minutos_total', y='ligacoes_total', hue='score_geral')
plt.title('Divisão de clientes - Segmentação por Kmeans (3 clusters)', size=15)
# Salvando as informações dos clusters num arquivo CSV.
clusters.to_csv('clusters.csv')
# Analisando informações estatísticas dos scores.
clusters.groupby('score_geral')[['ligacoes_total', 'minutos_total']].describe()
Aqui podemos retornar ao Kmeans e aumentarmos a quantidade de cluster para maior divisão dos clientes ou podemos subdividir os clientes em subcategorias dentro de cada score.
Conforme vemos no gráfico Divisão de clientes acima, podemos ver que temos 5 categorias de clientes sendo que as categorias do meio (B, C e D) possuem sub grupos.
Vejamos como os clientes foram divididos:
Score A: Aqui temos os clientes que mais fazem ligações e são os que mais demoram em ligações. Entretanto é o segundo grupo com menos clientes.
Score B: aqui temos o grupo com clientes que fazem um número médio de ligações, porém ficam bastante tempo nessas ligações e temos os clientes que ficam um tempo médio nas ligações, porém fazem um número alto de ligações. É o segundo grupo com mais clientes.
Score C: aqui temos o grupo com mais clientes e também o que apresenta maior variação de perfis nos clientes. Temos três subcategorias:
Score D: aqui temos o quarto grupo de clientes. Nele estão os clientes que fazem poucas ligações e ficam um tempo mediano em cada ligação e os clientes que fazem num número mediano de ligações, mas ficam pouco tempo nessas ligações.
Score E: grupo com menor quantidade de clientes, entretanto é o grupo que contém os clientes que menos fazem ligações e menos tempo gastam nessas ligações.
Podem ser realizadas diferentes estratégias para cada grupo e subgrupo de clientes de acordo com o objetivo da empresa.
Passaremos à etapa de engenharia de atributos para então seguirmos para a criação de uma regressão logística. Para tal, precisamos realizar os seguintes passos:
Vamos criar algumas funções para nos ajudar com as 3 primeiras etapas acima.
def treino_teste(x_data, y_data, siz=0.3, state=0):
# Importando o pacote necessário
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(x_data, y_data, test_size=siz, random_state=state)
return X_train, X_test, y_train, y_test
def dummyes(data):
# criando uma cópia dos dados.
dados = data.copy()
# Importando o pacote necessário
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder() # Criando uma instância dele
dummy_columns = [] # Criando uma lista para salvarmos as variáveis com
for column in dados.columns:
if dados[column].dtype == object:
if dados[column].nunique() == 2:
# aplicando o Label Encoder às variáveis categóricas binárias
dados[column] = le.fit_transform(dados[column])
else:
dummy_columns.append(column)
# Aplicando a função get dummies às variáveis categóricas não binárias.
dados = pd.get_dummies(data = dados, columns = dummy_columns)
return dados
def balance(x_data, y_data, state=0):
# Importando o pacote necessário
from imblearn.over_sampling import SMOTE
# Criando uma variável com as colunas dos dados.
colunas = x_data.columns
# criando uma instância do SMOTE
os = SMOTE(random_state=state)
x_smoted, y_smoted = os.fit_resample(x_data, y_data)
x_df = pd.DataFrame(x_smoted, columns=colunas)
y_df = pd.DataFrame(y_smoted, columns=['churn'])
xy_df = pd.concat([x_df, y_df], axis=1)
return xy_df
# criando variáveis separadas às variáveis dependentes e independente.
x = dados_treino_bruto.loc[:, dados_treino_bruto.columns != 'churn']
y = dados_treino_bruto[['churn']]
# Seperando os dados em treino e teste (70% - 30%)
x_treino, x_teste, y_treino, y_teste = treino_teste(x, y)
# Criando variáveis dummies
x_treino_dummie = dummyes(x_treino)
x_teste_dummie = dummyes(x_teste)
# aplicando o balanceamento aos dados de treino e teste.
xy_treino_smoted = balance(x_treino_dummie, y_treino)
xy_teste_smoted = balance(x_teste_dummie, y_teste)
Agora podemos passar à última etapa de nossos dados que que trata da selação das melhores variáveis para criação do modelo de regressão logística.
Iniciaremos removendo as variáveis com Multicolinearidade, para isso consideraremos uma alta relação quando as variáveis tiverem uma correlação superior à 0.7
Simbora!
# Criando uma variável do tipo set para armazenagem das variáveis altamente relacionáveis
treino_corr = xy_treino_smoted.corr()
colunas_rela = set()
for index_col, col in enumerate(treino_corr.columns):
for index_ind, ind in enumerate(treino_corr.index):
if (abs(treino_corr.loc[ind, col]) > 0.7 and index_col>index_ind):
colunas_rela.add(col)
# Verificando a quantidade de variáveis.
print('Número de variáveis altamente relacionáveis: ', len(colunas_rela))
# Imprimindo as variáveis.
print(f'\nVariáveis: {colunas_rela}')
Faremos a remoção das variáveis altamente relacionáveis e seguiremos com a criação da regressão logística para análise.
# Removendo as variáveis
xy_treino_smoted.drop(colunas_rela, axis=1, inplace=True)
# Renomeando as variáveis e criando fórmula para criação da regressão logística.
xy_treino_smoted.columns = [x.replace(' ','_') for x in xy_treino_smoted.columns]
variaveis = 'tempo_conta'
for coluna in xy_treino_smoted.columns:
if coluna not in['churn', 'tempo_conta']:
variaveis = variaveis + ' + ' + coluna
# Importando os pacotes necessários
import statsmodels.formula.api as smf
import statsmodels.api as sm
# Criado o modelo e imprimindo
ols = smf.ols(formula=f'churn ~ {variaveis}', data=xy_treino_smoted).fit()
print(ols.summary())
Podemos ver que nem todas as variáveis são interessantes ao nosso modelo. Para termos uma significância de 0.05 utilizaremos apenas as variáveis om p-values inferiores à 0.05.
# criando nova variável com as colunas
variaveis = 'srv_internacional'
for coluna in xy_treino_smoted.columns:
if coluna in ols.pvalues[ols.pvalues<0.05].index and coluna!='srv_internacional':
variaveis = variaveis + ' + ' + coluna
# Criando versão 2 do modelo e imprimindo
ols_v2 = smf.ols(formula=f'churn ~ {variaveis}', data=xy_treino_smoted).fit()
print(ols_v2.summary())
Podemos ver que nosso R-squared é de ~73%, ou seja, nossas variáveis independentes estão explicando 73% da média de churn. Podemos aumentar o número de nossas variáveis para vermos se conseguimos melhorar nosso R_squared, porém ao projeto prosseguiremos com o que temos.
Ao projeto seguiremos com as variáveis que temos. Vejamos algumas informações da tabela acima:
Com a remoção das variáveis conseguimos notar que o p-value de algumas variáveis alteraram. Entretanto seguiremos com todas as variáveis.
def treina_avalia(data, y, algoritmo, folds=10, seed=2):
# Importando variáveis externas
global ols_v2
# Pacotes para transformação dos dados
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler
# Pacotes para avaliação dos modelos criados.
from sklearn.model_selection import KFold
from sklearn.model_selection import cross_val_score
# Separando a variável independente das demais.
x = data.loc[:, ols_v2.params[1:].index].values
y = data[y]
x = MinMaxScaler(feature_range = (0, 1)).fit_transform(x)
x = StandardScaler().fit_transform(x)
# Criando e treinando o modelo.
modelo = algoritmo.fit(x, y)
# Criando o separador dos dados em folds.
separador = KFold(folds, shuffle=True, random_state=seed)
# Criando o modelo.
Validation = cross_val_score(modelo, x, y, cv=separador, scoring='accuracy')
# criando um dataframe para computar as informações.
resultados = pd.DataFrame({'Acuracia': Validation.mean(),
'Desvio': Validation.std()}, index=[str(algoritmo)[0:-2]])
print(resultados)
return modelo
def validation_predict(data, y, algoritmo):
# importando os pacotes necessiários
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import confusion_matrix
from sklearn.metrics import accuracy_score
from sklearn.metrics import roc_curve
from sklearn.metrics import auc
from sklearn.metrics import roc_auc_score
from sklearn.metrics import plot_roc_curve
# Separando a variável independente das demais.
x_teste = data.loc[:, ols_v2.params[1:].index]
y_teste = data[y]
# aplicando transformações aos dados.
x_teste = MinMaxScaler(feature_range = (0, 1)).fit_transform(x_teste)
x_teste = StandardScaler().fit_transform(x_teste)
# Realizando as predições.
predicts = algoritmo.predict(x_teste)
# Verificando a AUC do modelo
fpr, tpr, thresholds = roc_curve(y_teste, predicts, pos_label=1)
# Verificando estatítiscas do modelo.
print('\n\nAcurácia aos dados de teste:\n\n')
print('Accuracy: ', round(accuracy_score(y_teste, predicts), ndigits=3)*100)
print('\nConfusion Matrix:\n', confusion_matrix(y_teste, predicts))
print('\nAUC (Area Under Curve): ', round(auc(fpr, tpr), ndigits=3)*100)
print('\nROC CURVE:')
plot_roc_curve(algoritmo, x_teste, y_teste)
plt.show()
# Limperaremos os dados de teste conforme fizemos com os de treino para seguirmos os próximos passos.
# Removendo as variáveis
xy_teste_smoted.drop(colunas_rela, axis=1, inplace=True)
# Renomeando as variáveis e criando fórmula para criação da regressão logística.
xy_teste_smoted.columns = [x.replace(' ','_') for x in xy_teste_smoted.columns]
Aqui utilizaremos dois algoritmos para criação e validação. Um deles será a própria regressão logística e o segundo será o o HistGradientBoostingClassifier.
Simbora ver o que conseguimos.
# Importando o pacote para regressão logística.
from sklearn.linear_model import LogisticRegression
# criando, treinando e avaliando o algoritmo.
logreg = treina_avalia(xy_treino_smoted, 'churn', LogisticRegression())
# Avaliando com os dados de teste.
validation_predict(xy_teste_smoted, 'churn', logreg)
# Importando o pacote para regressão logística.
from sklearn.experimental import enable_hist_gradient_boosting
from sklearn.ensemble import HistGradientBoostingClassifier
# criando, treinando e avaliando o algoritmo.
histgradboost = treina_avalia(xy_treino_smoted, 'churn', HistGradientBoostingClassifier())
# Avaliando com os dados de teste.
validation_predict(xy_teste_smoted, 'churn', histgradboost)
Podemos ver que ambos os algoritmos tiveram um desempenho similar quando os aplicamos aos dados de teste. Seguiremos com a regressão logística para sabermos a probabilidade de um determinado cliente dar churn utilizando um novo conjunto de dados.
Precisaremos realizar as mesmas transformações realizadas aos dados anteriormente.
# Removendo a coluna Unnamed
dados_teste_bruto.drop('Unnamed: 0', axis=1, inplace=True)
# Agregando o nome e região dos estados
dados_teste_bruto = dados_teste_bruto.merge(states_name, sort=True)
# removendo a coluna state antiga
dados_teste_bruto = dados_teste_bruto.drop('state', axis=1)
# Renomeando as colunas.
dados_teste_bruto.columns = ['tempo_conta', 'codigo_area', 'srv_internacional', 'srv_caixa_postal', 'mensagens_voz',
'minutos_lig_manha', 'ligacoes_manha', 'taxa_lig_manha', 'minutos_lig_tarde', 'ligacoes_tarde',
'taxa_lig_tarde', 'minutos_lig_noite', 'ligacoes_noite', 'taxa_lig_noite', 'minutos_lig_internacional',
'ligacoes_internacional', 'taxa_lig_internacional', 'ligacoes_SAC', 'churn', 'estado', 'regiao']
# Criando uma coluna com os minutos totais
dados_teste_bruto['minutos_total'] = dados_teste_bruto[[col for col in dados_teste_bruto.columns \
if 'minutos' in col]].sum(axis=1)
# criando uma coluna com o total de ligações
dados_teste_bruto['ligacoes_total'] = dados_teste_bruto[[col for col in dados_teste_bruto.columns \
if 'ligacoes' in col]].sum(axis=1)
# criando uma coluna com o total de taxa paga
dados_teste_bruto['taxa_total'] = dados_teste_bruto[[col for col in dados_teste_bruto.columns \
if 'taxa' in col]].sum(axis=1)
conditions = [
(dados_teste_bruto['srv_internacional'] == 'yes') & (dados_teste_bruto['srv_caixa_postal'] == 'yes'),
(dados_teste_bruto['srv_internacional'] == 'yes') | (dados_teste_bruto['srv_caixa_postal'] == 'yes'),
(dados_teste_bruto['srv_internacional'] == 'no') & (dados_teste_bruto['srv_caixa_postal'] == 'no')]
choices = [int(2), int(1), int(0)]
dados_teste_bruto['Qntd_servicos'] = np.select(conditions, choices, default=np.nan)
dados_teste_bruto['churn'] = [0 if x=='no' else 1 for x in dados_teste_bruto['churn']]
for coluna in numericas[15:17]:
kmeans = KMeans(n_clusters=3).fit(dados_teste_bruto[[coluna]])
dados_teste_bruto[str('cluster_'+coluna)] = kmeans.predict(dados_teste_bruto[[coluna]])
# Verificandoo cluster de minutos
dados_teste_bruto.groupby('cluster_minutos_total')['minutos_total'].describe()
# Verificandoo cluster de ligações
dados_teste_bruto.groupby('cluster_ligacoes_total')['ligacoes_total'].describe()
# Reordenando o cluster.
dados_teste_bruto['cluster_minutos_total'] = dados_teste_bruto['cluster_minutos_total'].map({0:2, 1:0, 2:1})
dados_teste_bruto['cluster_ligacoes_total'] = dados_teste_bruto['cluster_ligacoes_total'].map({0:0, 1:2, 2:1})
# Criando uma nova coluna com o score geral.
dados_teste_bruto['score_geral'] = dados_teste_bruto['cluster_minutos_total'] + dados_teste_bruto['cluster_ligacoes_total']
dados_teste_bruto.groupby('score_geral')[['minutos_total', 'ligacoes_total']] \
.mean().sort_values(['ligacoes_total', 'minutos_total'])
# criando variáveis separadas às variáveis dependentes e independente.
y = dados_teste_bruto[['churn']]
# Aplicando criando variáveis dummies às variáveis categóricas
teste_dummie = dummyes(dados_teste_bruto)
# aplicando o balanceamento aos dados de treino e teste.
final_df = balance(teste_dummie, y)
# Removendo as variáveis
final_df.drop(colunas_rela, axis=1, inplace=True)
# Renomeando as variáveis e criando fórmula para criação da regressão logística.
final_df.columns = [x.replace(' ','_') for x in final_df.columns]
# importando pacotes para transformação
from sklearn.preprocessing import MinMaxScaler
from sklearn.preprocessing import StandardScaler
x_final = final_df.loc[:, ols_v2.params[1:].index].values
# Aplicando as transformações aos dados
x_final = MinMaxScaler(feature_range = (0, 1)).fit_transform(x_final)
x_final = StandardScaler().fit_transform(x_final)
# criando um novo dataframe contendo os IDs dos clientes.
churn_prob = pd.DataFrame({'ID_cliente': range(1, len(x_final)+1)})
# Verificando a probabilidade de cada cliente dar churn
churn_prob['Probabilidade_churn'] = logreg.predict_proba(x_final)[:,1]*100
# verificando a probabilidade dos clientes
churn_prob = churn_prob.set_index('ID_cliente')
churn_prob.head()
# salvando os algoritmos e o resultado final em disco.
import pickle
pickle.dump(logreg, open('logisticregression.sav', 'wb'))
churn_prob.to_csv('probs_churn.csv', index=False)